/**
 * Copyright (c) iwindplus Technologies Co., Ltd.2024-2030, All rights reserved.
 */

package com.iwindplus.boot.redis.aspect;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.iwindplus.boot.domain.constant.CommonConstant.SymbolConstant;
import com.iwindplus.boot.domain.context.UserContextHolder;
import com.iwindplus.boot.domain.enums.BizCodeEnum;
import com.iwindplus.boot.domain.exception.BizException;
import com.iwindplus.boot.domain.vo.UserBaseVO;
import com.iwindplus.boot.redis.domain.annotation.RedisRateLimiter;
import com.iwindplus.boot.redis.domain.constant.RedisConstant;
import com.iwindplus.boot.redis.domain.property.RedisProperty;
import com.iwindplus.boot.redis.manager.RedissonManager;
import com.iwindplus.boot.redis.service.RateLimiterKeyResolver;
import com.iwindplus.boot.util.DatesUtil;
import jakarta.annotation.Resource;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import net.dreamlu.mica.core.utils.StringUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RateType;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.util.StringUtils;

/**
 * 限流切面.
 *
 * @author zengdegui
 * @since 2024/07/06 12:18
 */
@Slf4j
@Aspect
public class RedisRateLimiterAspect {

    @Resource
    private RedissonManager redissonManager;

    @Resource
    private KeyGenerator keyGenerator;

    @Resource
    private CacheProperties cacheProperties;

    @Resource
    private RedisProperty property;

    @Resource
    private Map<String, RateLimiterKeyResolver> keyResolvers;

    @Pointcut("@within(com.iwindplus.boot.redis.domain.annotation.RedisRateLimiter) || @annotation(com.iwindplus.boot.redis.domain.annotation.RedisRateLimiter)")
    private void pointCutMethod() {
    }

    /**
     * Before 切面
     *
     * @param joinPoint joinPoint
     */
    @Before("pointCutMethod()")
    public void beforePointCut(JoinPoint joinPoint) {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        RedisRateLimiter rateLimiter = method.getAnnotation(RedisRateLimiter.class);
        if (Boolean.FALSE.equals(this.property.getRateLimiter().getEnabled()) || Boolean.FALSE.equals(rateLimiter.enabled())) {
            return;
        }
        final long beginMillis = System.currentTimeMillis();
        log.info("Redis限流，开始时间={}", DatesUtil.parseDate(beginMillis, DatePattern.NORM_DATETIME_MS_PATTERN));

        final RateType rateType = rateLimiter.rateType();
        final long rate = rateLimiter.rate();
        final long rateInterval = rateLimiter.rateInterval();
        final TimeUnit rateIntervalUnit = rateLimiter.rateIntervalUnit();

        Class<? extends RateLimiterKeyResolver> aClass = rateLimiter.keyResolver();
        final String simpleName = aClass.getSimpleName();
        final String beanName = StrUtil.lowerFirst(simpleName);
        RateLimiterKeyResolver keyResolver = this.keyResolvers.get(beanName);

        try {
            String key = this.getLockKey(joinPoint, rateLimiter, keyResolver);

            boolean status = this.rateLimiter(rateType, key, rate, rateInterval, rateIntervalUnit);

            log.info("Redis限流，key={}，状态={}，限流次数={}, 限流速率={}, 限流速率时间单位={}",
                key, status, rate, rateInterval, rateIntervalUnit);

            if (!status) {
                throw new BizException(BizCodeEnum.REQUEST_TOO_FAST);
            }
        } finally {
            final long endMillis = System.currentTimeMillis();
            log.info("Redis限流，限流次数={}, 限流速率={}, 限流速率时间单位={}，结束时间={}，总执行毫秒数={}",
                rate, rateInterval, rateIntervalUnit,
                DatesUtil.parseDate(endMillis, DatePattern.NORM_DATETIME_MS_PATTERN),
                endMillis - beginMillis);
        }
    }

    private String getLockKey(JoinPoint joinPoint, RedisRateLimiter rateLimiter, RateLimiterKeyResolver keyResolver) {
        String resolver = keyResolver.resolver(joinPoint, this.keyGenerator, rateLimiter);
        if (CharSequenceUtil.isBlank(resolver)) {
            resolver = StringUtil.getUUID();
        }
        StringBuilder sb = new StringBuilder();
        CacheProperties.Redis redisProperties = this.cacheProperties.getRedis();
        if (StringUtils.hasText(redisProperties.getKeyPrefix())) {
            sb.append(redisProperties.getKeyPrefix());
        }
        sb.append(RedisConstant.RATE_LIMITER_KEY);
        if (rateLimiter.useUserId()) {
            UserBaseVO context = UserContextHolder.getContext();
            if (ObjectUtil.isNotEmpty(context)) {
                sb.append(context.getUserId()).append(SymbolConstant.COLON);
            }
        }
        String[] names = rateLimiter.names();
        if (ArrayUtil.isNotEmpty(names)) {
            String name = Arrays.asList(names).stream().collect(Collectors.joining(SymbolConstant.UNDERLINE));
            sb.append(name).append(SymbolConstant.COLON);
        }
        sb.append(resolver);
        return sb.toString();
    }

    private boolean rateLimiter(RateType rateType, String key, long rate,
        long rateInterval, TimeUnit rateIntervalUnit) {
        rate = Math.max(rate, 1);
        rateInterval = Math.max(rateInterval, 1);

        return redissonManager.tryAcquire(key, rateType, rate, Duration.of(rateInterval, rateIntervalUnit.toChronoUnit()));
    }
}

